﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Xml.XPath;
using GianosNET.Common.Classes;
using GianosNET.Common.Interfaces;
using GianosNET.ServerApp.Implementation;
using NLog;
using GianosNET.ServerApp.Properties;
using SQLPXBase;

namespace GianosNET.ServerApp
{
    internal class Program
    {
        #region Parametri di avvio

        private static bool autostart = false;

        #endregion

        #region Intercetta il Kill dell'applicazione

        [DllImport("Kernel32")]
        public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);

        // control messages  
        public enum CtrlTypes
        {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT = 1,
            CTRL_CLOSE_EVENT = 2,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT = 6
        }

        //delegate type to be used of the handler routine  
        public delegate bool HandlerRoutine(CtrlTypes CtrlType);

        private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
        {
            if (ctrlType != CtrlTypes.CTRL_C_EVENT && ctrlType != CtrlTypes.CTRL_BREAK_EVENT)
                return false;

            if (logger != null)
                logger.Debug("Iniziato shutdown posticipato del sistema operativo");

            if (info != null)
                foreach (ServiceInfo svc in info.Services())
                    info.ServiceStop(svc.SvcId);
            else
                logger.Debug("Errore: info is NULL");

            System.Threading.Thread.Sleep(20000);

            // ** SHUTDOWN **
            if (logger != null)
                logger.Info("Gianos.NET terminated.");
            if (statusProxyMgr != null)
                statusProxyMgr.GetProxy().NotifyRootStatus("STOPPED");

            return true; // Consente lo shutdown dell'applicazione
        }

        #endregion

        // NLog
        internal static readonly Logger logger = LogManager.GetCurrentClassLogger();

        //private static RemotedServiceManager mgr;
        internal static ServerInfo info;
        private static Guid serverId;
        private static ProxyManager<sqlStatus> statusProxyMgr = null;

        private static ServiceHost serviceHost;

        // modules
        private static List<ModuleConfigSection.ModuleConfiguration> modules;

        private static void Main(string[] args)
        {
            #region Intercetta un eventuale chiusura dell'applicazione

            HandlerRoutine killHandler = ConsoleCtrlCheck; // Setta l'handler da chiamare sull'exit
            SetConsoleCtrlHandler(killHandler, true); // Setta l'handler come oggetto da chiamre in caso di shutdown

            #endregion

            AnalizzaArgomenti(args);

            //SystemEvents.SessionEnding += new SessionEndingEventHandler(SystemEvents_SessionEnding);


            try
            {
                #region Status DB

                if (Settings.Default.UseStatusDB)
                {
                    statusProxyMgr = new ProxyManager<sqlStatus>(new MSSQLServerConnectionString(
                        Settings.Default.StatusDBServer,
                        Settings.Default.StatusDBDatabase,
                        Settings.Default.StatusDBUsername,
                        Settings.Default.StatusDBPassword
                        ));

                    statusProxyMgr.GetProxy().CleanStatus();
                }

                #endregion

                if (serviceHost != null)
                    serviceHost.Close();

                // Informazioni sul server
                Assembly a = Assembly.GetExecutingAssembly();
                System.Diagnostics.FileVersionInfo vi = System.Diagnostics.FileVersionInfo.GetVersionInfo(a.Location);

                logger.Info("Gianos.NET started: Ver. {0}", vi.ProductVersion);
                Console.WriteLine("Gianos.NET (WCF version) - AppServer v. {0}", vi.ProductVersion);

                logger.Trace("Gianos.NET started: Ver. {0}", vi.ProductVersion);

                if (!Settings.Default.EnableAutoStart)
                    Console.WriteLine("AUTOSTART SERVICES DISABLED");

                // registrazione del modulo di info
                info = new ServerInfo {ServerVersion = vi.ProductVersion};

                // Create a ServiceHost for the RemotedService type 
                serviceHost = new ServiceHost(typeof (RemotedServiceManager),
                    new[] {new Uri(Settings.Default.ServerUrl)})
                {
                    OpenTimeout = new TimeSpan(0, 0, 10),
                    CloseTimeout = new TimeSpan(0, 0, 10)
                };


                ServiceDebugBehavior debug = serviceHost.Description.Behaviors.Find<ServiceDebugBehavior>();
                // if not found - add behavior with setting turned on 
                if (debug == null)
                {
                    serviceHost.Description.Behaviors.Add(
                        new ServiceDebugBehavior() {IncludeExceptionDetailInFaults = true});
                }
                else
                {
                    // make sure setting is turned ON
                    if (!debug.IncludeExceptionDetailInFaults)
                    {
                        debug.IncludeExceptionDetailInFaults = true;
                    }
                }


                NetTcpBinding b = new NetTcpBinding();
                b.Security.Mode = SecurityMode.None;

                //BasicHttpBinding b = new BasicHttpBinding();
                //b.Security.Mode = BasicHttpSecurityMode.None;

                // Add an endpoint to the service.
                serviceHost.AddServiceEndpoint(typeof (IRemotedServiceManager), b, "");

                // Open the ServiceHostBase to create listeners and start 
                // listening for messages.
                serviceHost.Open();

                foreach (Uri uri in serviceHost.BaseAddresses)
                {
                    logger.Info("Server started. Listening on: {0}", uri);
                    Console.WriteLine("Server started. Listening on: {0}", uri);
                }
                Console.WriteLine();

                #region registrazione dei moduli

                // This code allows to load module dependencies from a different path than the executing path
                if (!string.IsNullOrEmpty(Settings.Default.DefaultDependenciesFolder))
                {
                    AppDomain currentDomain = AppDomain.CurrentDomain;
                    currentDomain.AssemblyResolve += currentDomain_AssemblyResolve;
                }

                ModuleConfigSection modulesConfigSection =
                    ConfigurationManager.GetSection("GianosNET_Modules") as ModuleConfigSection ??
                    new ModuleConfigSection();

                modules = new List<ModuleConfigSection.ModuleConfiguration>();
                modules.AddRange(modulesConfigSection.Config);

                if (Settings.Default.AutodiscoverModules)
                {
                    Console.WriteLine("Autodiscovering enabled.");

                    AutodiscoverModules();
                }

                foreach (ModuleConfigSection.ModuleConfiguration moduleConfiguration in modules)
                    RegisterRemoteObject(

                        // Il path dei moduli può essere indicato come ~nomefile.dll (in questo caso, il file viene cercato nella directory di default dei moduli), oppure con il path completo
                        moduleConfiguration.AssemblyPath.StartsWith("~")
                            ? string.Format("{0}{1}", Settings.Default.DefaultModulesPath,
                                moduleConfiguration.AssemblyPath.Substring(1))
                            : moduleConfiguration.AssemblyPath,
                        moduleConfiguration.AssemblyTypeName,
                        moduleConfiguration.Name,
                        moduleConfiguration.ConfigArea
                        );


                #endregion

                if (Settings.Default.UseStatusDB)
                    statusProxyMgr.GetProxy().NotifyRootStatus("OK");


                using (FileSystemWatcher watcher = new FileSystemWatcher()
                {
                    Path = Settings.Default.ModuleConfigurationPath,
                    IncludeSubdirectories = false,
                    Filter = "*.config"
                })
                {
                    watcher.Changed += watcher_Changed;
                    watcher.EnableRaisingEvents = Settings.Default.EnableConfigRefresh;


                    // Standby
                    Console.WriteLine("Running. Press CTRL+C to end publication.");

                    int i = 0;
                    while (true)
                    {
                        // Se abilitata la segnalazione su Sql (per il Nagios), segnala ogni 60 secondi lo status OK
                        if (statusProxyMgr != null)
                        {
                            if (i > (60000/Settings.Default.IdleTick)) // ogni 60 secondi
                            {
                                i = 0;
                                if (Settings.Default.UseStatusDB)
                                    statusProxyMgr.GetProxy().NotifyRootStatus("OK");
                                Console.Write(".");
                            }
                            else
                                i++;
                        }
                        System.Threading.Thread.Sleep(Settings.Default.IdleTick); // 1 secondo
                    }
                }
            }
            catch (Exception ex)
            {
                logger.ErrorException("Errore nell'esecuzione dei remoted services", ex);
                if (Settings.Default.UseStatusDB)
                    statusProxyMgr.GetProxy().NotifyRootStatus("ERROR");
            }
            return;
        }

        private static void AutodiscoverModules()
        {
            logger.Info(string.Format("Autodiscovering modules: path={0}", Settings.Default.DefaultModulesPath));

            // Checks all dll files in "DefaultModulesPath" for autodiscovering
            string[] files = Directory.GetFiles(Settings.Default.DefaultModulesPath, "*.dll");
            foreach (string file in files)
            {
                logger.Info(string.Format("Checking: {0}", file));

                try
                {
                    Assembly assembly = Assembly.LoadFile(Path.GetFullPath(file));
                    Type[] types = null;
                    try
                    {
                        types = assembly.GetTypes();
                        logger.Info(string.Format("Loaded: {0} types", types.Length));
                    }
                    catch (ReflectionTypeLoadException ex)
                    {
                        logger.Error("Managed exception loading module types", ex);
                        logger.Info("Alternative types loaded: {0}", ex.Types.Length);

                        types = ex.Types;
                    }
                    catch (Exception ex)
                    {
                        logger.Error("Error loading module types", ex);
                    }

                    if (types != null)
                        foreach (Type t in types)
                        {
                            logger.Info("Checking type: {0}", t.FullName);

                            if (typeof(LoopingService).IsAssignableFrom(t) && !t.IsAbstract)
                            {
                                logger.Info(string.Format("Found eligible type: {0}", t.FullName));

                                // Only modules with "DefaultConfigArea" defined can be autodiscovered
                                PropertyInfo p = t.GetProperty("DefaultConfigArea",
                                    BindingFlags.NonPublic | BindingFlags.Static);
                                if (p != null && !string.IsNullOrEmpty(p.GetValue(null, null) as string))
                                {
                                    ModuleConfigSection.ModuleConfiguration cfg = new ModuleConfigSection.
                                        ModuleConfiguration()
                                    {
                                        AutoDiscovered = true,
                                        AssemblyPath = file,
                                        Name = t.FullName,
                                        AssemblyTypeName = t.FullName,
                                        ConfigArea = p.GetValue(null, null) as string
                                    };

                                    logger.Info(string.Format("Config area: {0}", cfg.ConfigArea));

                                    // Only adds autodiscovered if no overrides are present for the same type
                                    bool exists =
                                        modules.Any(
                                            moduleConfiguration =>
                                                moduleConfiguration.AssemblyTypeName == cfg.AssemblyTypeName);

                                    // Skips if exists
                                    if (exists)
                                    {
                                        logger.Info("Already exists: skipped.");
                                        continue;
                                    }

                                    // Check if autodiscovered config area is present in config file, else skips
                                    string configFile = Path.Combine(Settings.Default.ModuleConfigurationPath,
                                        cfg.ConfigArea + ".config");

                                    //ConfigSection o =
                                    //    ConfigurationManager.GetSection(cfg.ConfigArea) as ConfigSection;
                                    bool isConfigValid = false;
                                    if (File.Exists(configFile))
                                    {
                                        using (StreamReader rd = File.OpenText(configFile))
                                        {
                                            XPathDocument doc = new XPathDocument(rd);
                                            XPathNavigator nav = doc.CreateNavigator();
                                            if (nav.Select("/" + cfg.ConfigArea) != null)
                                                isConfigValid = true;
                                        }
                                    }

                                    if (!isConfigValid)
                                    {
                                        logger.Info("Cannot find valid config file. Skipped.");
                                        continue;
                                    }

                                    // If all checks are ok, add the new autodiscovered modules
                                    modules.Add(cfg);
                                }
                                else
                                {
                                    logger.Info("No config area: skipped");
                                }
                            }
                        }
                }
                catch (Exception ex)
                {
                    logger.Error("Error loading assembly", ex);
                }
            }
        }

        private static void watcher_Changed(object sender, FileSystemEventArgs e)
        {
            if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created)
            {
                string configname = Path.GetFileNameWithoutExtension(e.FullPath);
                
                logger.Info(string.Format("Config file changed: halting services of type {0}", configname));
                int res = info.UnregisterServices(configname);
                logger.Info(string.Format("{0} services halted", res));

                AutodiscoverModules();

                // If any module matches config file, re-register services
                ModuleConfigSection.ModuleConfiguration moduleConfiguration =
                    modules.FirstOrDefault(m => m.ConfigArea == configname);
                if (moduleConfiguration != null)
                {
                    RegisterRemoteObject(
                        // Il path dei moduli può essere indicato come ~nomefile.dll (in questo caso, il file viene cercato nella directory di default dei moduli), oppure con il path completo
                        moduleConfiguration.AssemblyPath.StartsWith("~")
                            ? string.Format("{0}{1}", Settings.Default.DefaultModulesPath,
                                moduleConfiguration.AssemblyPath.Substring(1))
                            : moduleConfiguration.AssemblyPath,
                        moduleConfiguration.AssemblyTypeName,
                        moduleConfiguration.Name,
                        moduleConfiguration.ConfigArea
                        );
                }

                info.ForceClientsRefresh();
            }
        }

        private static Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {

            string assemblyPath = Path.Combine(Settings.Default.DefaultDependenciesFolder,
                new AssemblyName(args.Name).Name + ".dll");
            if (File.Exists(assemblyPath))
                return Assembly.LoadFrom(assemblyPath);

            return null;

        }

        private static void AnalizzaArgomenti(string[] args)
        {
            foreach (string s in args)
            {
                switch (s.ToLowerInvariant())
                {
                    case "/autostart":
                        autostart = true;
                        break;
                }
            }
        }

        #region Metodi di registrazione dei servizi

        private static void RegisterRemoteObject(Type t, string name, string configArea)
        {
            // Reading configuration file
            ConfigSection o = new ConfigSection();
            try
            {
                string configFile = Path.Combine(Settings.Default.ModuleConfigurationPath,
                    configArea + ".config");
                if (File.Exists(configFile))
                    using (StreamReader rd = File.OpenText(configFile))
                    {
                        XPathDocument doc = new XPathDocument(rd);
                        XPathNavigator nav = doc.CreateNavigator();

                        nav = nav.SelectSingleNode("/" + configArea);
                        if (nav != null)
                        {
                            int i = 0;
                            foreach (XPathNavigator n in nav.Select("config"))
                            {
                                string nm = n.GetAttribute("name", "");
                                if (string.IsNullOrEmpty(nm))
                                    nm = i.ToString();

                                Hashtable dict = new Hashtable();
                                foreach (XPathNavigator k in n.Select("add[@key]"))
                                    dict.Add(
                                        k.GetAttribute("key", ""),
                                        k.GetAttribute("value", "")
                                        );

                                o.Configs.Add(nm, dict);
                                i++;
                            }
                        }
                    }
            }
            catch (Exception ex)
            {
                logger.Warn("Error loading config file", ex);
            }

            if (o.Configs.Count > 0)
                foreach (KeyValuePair<string, Hashtable> pair in o.Configs)
                {
                    string nm = o.Configs.Count > 1 ? string.Format("{0} ({1})", name, pair.Key) : name;
                    Hashtable cfg = pair.Value;
                    if (!cfg.ContainsKey("enabled") || cfg["enabled"] as string != "true")
                    {
                        Console.WriteLine("Servizio remoto: {0} [DISABILITATO]", nm);
                        if (statusProxyMgr != null)
                            statusProxyMgr.GetProxy().NotifyServiceStatus(nm, "DISABLED");
                        return;
                    }

                    RemotedClass c =
                        Activator.CreateInstance(t, cfg, statusProxyMgr, Settings.Default.EnableAutoStart, info) as
                            RemotedClass;
                    if (c != null)
                    {
                        c.InstanceName = o.Configs.Count > 1 ? pair.Key : "";
                        RegisterRemoteObject(c);
                    }
                }
            else
                Console.WriteLine("Servizio remoto: {0} [DISABILITATO - NO CONFIG]", name);
        }

        private static void RegisterRemoteObject(RemotedClass c)
        {
            info.RegisterService(c);
            c.SetLogger(logger);

            if (autostart && Settings.Default.EnableAutoStart)
                c.Start();

            Console.WriteLine(string.Format("Registrato servizio: {0} - {1}", c.ServiceName, c.UniqueIdentifier));
        }

        private static void RegisterRemoteObject(string assemblyPath, string assemblyTypeName, string name,
            string configArea)
        {
            FileInfo info = new FileInfo(Path.GetFullPath(assemblyPath));
            if (!info.Exists)
                throw new Exception("Assembly file not found");

            Assembly assembly = Assembly.LoadFile(info.FullName);
            Type t = assembly.GetType(assemblyTypeName);

            RegisterRemoteObject(t, name, configArea);
        }

        #endregion
    }
}